5.2 Delegaten  
5.2.1 Einführung in das Prinzip der Delegaten  
Das Prinzip der Delegaten ist nicht ganz neu, wohl aber der Begriff an sich. »Delegate« ist das englische Wort für »delegieren« – etwa »weiterleiten«. Tatsächlich leitet ein Delegat weiter, er leitet nämlich einen Methodenaufruf an eine bestimmte Methode weiter.
Die Technik, die sich dahinter verbirgt, wird in der Sprache C auch als Funktionszeiger bezeichnet. Zeigertechnik und .NET – das passt eigentlich nicht zusammen. Die Zeigertechnik – so interessant sie auch sein mag – birgt einige Nachteile in sich: Sie ist schwierig zu lernen, sie ist sehr komplex, die Programme sind zu kompliziert. Ein falscher Einsatz führt nicht selten zu Speicherzugriffsfehlern und damit zum Absturz eines laufenden Programms. Nicht umsonst haben die .NET-Entwickler (und auch die von Java) die Zeigertechnologie gemieden wie der Teufel das Weihwasser.
Dennoch gibt es Aufgabenstellungen, bei denen kein Weg an der Zeigertechnik vorbeiführt – auch nicht unter .NET, wenn auch in einer nicht sofort offensichtlichen Form. Wie Sie wissen, basiert ausnahmslos alles im .NET Framework auf Objekten. Da verwundert es nicht, dass auch die Methodenzeiger in ein Objekt verpackt und als Delegat bezeichnet ihren Weg in die Laufzeitumgebung finden.
|
Ein Delegat ist ein Objekt, das den Zeiger auf eine Objektmethode beschreibt.
|
Bevor wir uns mit den Details von Delegaten beschäftigen, wollen wir uns zunächst an einem einfachen Beispiel die grundsätzliche Arbeitsweise verdeutlichen.
Die Operation, die von diesem Code ausgeführt wird, ist recht einfach: Der Anwender gibt zwei Zahlen an der Konsole ein und hat anschließend die Wahl, ob beide Zahlen addiert oder subtrahiert werden sollen. Das Resultat der Operation wird abhängig von der Wahl des Anwenders an der Konsole ausgegeben.
| ' --------------------------------------------------------------
|
| ' Beispiel: ...\Kapitel 5\EinfacherDelegat
|
| ' --------------------------------------------------------------
|
| ' Definition des Delegaten
|
| Public Delegate Function ProcessOperation(ByVal dblVar1 As Double, _
|
| ByVal dblVar2 As Double) As Double
|
| Module Module1
|
| Sub Main()
|
| ' Variable vom Typ des Delegaten
|
| Dim process As ProcessOperation
|
| ' Eingabe der Operanden
|
| Console.Write("Geben Sie den ersten Operanden ein: ")
|
| Dim input1 As Double = Console.ReadLine
|
| Console.Write("Geben Sie den zweiten Operanden ein: ")
|
| Dim input2 As Double = Console.ReadLine
|
| ' Wahl der Operation
|
| Console.Write("Welche Operation wollen Sie ausführen?")
|
| Console.WriteLine("Addition – (A)")
|
| Console.WriteLine("Subtraktion – (S)")
|
| Dim wahl As String = Console.ReadLine().ToUpper()
|
| ' in Abhängigkeit von der Wahl des Anwenders wird die
|
| ' Variable 'process' mit einem Zeiger auf die
|
| ' auszuführende Methode initialisiert
|
| If wahl = "A" Then
|
| process = New ProcessOperation(AddressOf Addition)
|
| ElseIf (wahl = "S") Then
|
| process = New ProcessOperation(AddressOf Subtraktion)
|
| Else
|
| Console.Write("Ungültige Eingabe")
|
| Console.ReadLine()
|
| Return
|
| End If
|
| ' Aufruf der Operation 'Addition' oder 'Subtraktion'
|
| ' über den Delegaten
|
| Dim result As Double = process(input1, input2)
|
| Console.WriteLine("----------------------------------")
|
| Console.Write("Ergebnis = {0}", result)
|
| Console.ReadLine()
|
| End Sub
|
| Public Function Addition(ByVal x As Double, _
|
| ByVal y As Double) As Double
|
| Return x + y
|
| End Function
|
| Public Function Subtraktion(ByVal x As Double, _
|
| ByVal y As Double) As Double
|
| Return x – y
|
| End Function
|
| End Module
|
In Module1 sind neben dem obligatorischen Einstiegspunkt in die Laufzeit zwei Methoden definiert, die aus Main heraus aufgerufen werden und die beiden Operationen Addition und Subtraktion beschreiben.
Die Wahl, ob die beiden Zahlen addiert oder subtrahiert werden sollen, trifft der Anwender durch die Eingabe von A oder S an der Konsole. Um die Eingabe in Kleinschreibweise ebenfalls zu berücksichtigen, wird die Eingabe mit der Methode ToUpper der Klasse String in Großschreibweise umgewandelt.
| Dim wahl As String = Console.ReadLine().ToUpper()
|
Nachdem der Anwender seine Wahl getroffen hat, muss zuerst überprüft werden, wie diese ausgefallen ist, um entsprechend im Programmcode zu reagieren. Vermutlich hätten Sie eine solche Aufgabenstellung bisher wie folgt gelöst:
| Dim result As Double
|
| If wahl = "A" Then
|
| result = Addition(input1, input2)
|
| ElseIf wahl = "S" Then
|
| result = Subtraktion(input1, input2)
|
| End If
|
Es gibt keinen Zweifel daran, dass diese Implementierung natürlich auch zum richtigen Ergebnis führt.
Nun betrachten wir die entscheidenden Anweisungen der Lösung im Beispiel EinfacherDelegat:
| If wahl = "A" Then
|
| process = New ProcessOperation(AddressOf Addition)
|
| ElseIf (wahl = "S") Then
|
| process = New ProcessOperation(AddressOf Subtraktion)
|
| ...
|
| End If
|
| Dim result As Double = process(input1, input2)
|
Der Unterschied ist jetzt, dass wir das Ergebnis der Addition bzw. Subtraktion nun nicht mehr in den beiden Anweisungsblöcken der If-Struktur abrufen, sondern außerhalb derselben. Da aber außerhalb der If-Struktur die Wahl des Anwenders nicht bekannt sein kann, stehen wir vor der Frage, wie es möglich ist, eine Methode an einem Allgemeinplatz dynamisch aufrufen zu können. Die Antwort darauf ist prinzipiell nicht schwierig: Wir müssen einen Verweis auf die aufzurufende Methode in einer Variablen speichern, der später an einer beliebigen Stelle im Code ausgewertet werden kann.
Bisher kennen wir Verweise nur im Zusammenhang mit Objekten. Mit Objektverweisen werden zusammenhängende Datenblöcke im Hauptspeicher adressiert, in denen die Zustandsdaten eines ganz bestimmten Objekts beschrieben werden. Ein Verweis auf Programmcode ist im Grunde genommen nicht anders, zeigt aber auf Bytesequenzen, die anders interpretiert werden müssen – nämlich als ausführbarer Programmcode. Damit ist auch klar, dass ein Verweis auf Code anders definiert werden muss als der uns gebräuchliche Verweis auf Datenblöcke. Aus diesem Grund wurden in .NET die Delegaten eingeführt.
Wie schon oben erwähnt, kapselt ein Delegat den Zeiger auf eine Methode. Wir wissen zudem, dass .NET nur Objekte kennt. Sehen wir uns jetzt an, wie die Konstrukteure von Visual Basic 2005 diese beiden Anforderungen konzeptuell gelöst haben.
Im Code des Beispiels EinfacherDelegat wird mit
| Public Delegate Function ProcessOperation(ByVal dblVar1 As Double, _
|
| ByVal dblVar2 As Double) As Double
|
ein Delegat definiert. Diese Definition erinnert ein wenig an die Methodensignatur einer Methode namens ProcessOperation, die zwei Parameter vom Typ Double empfängt und als Rückgabewert einen Double liefert – nur ergänzt um das Schlüsselwort Delegate.
Ein Delegat kapselt den Zeiger auf eine Methode oder mit anderen Worten, er steht für einen beliebigen Methodenaufruf. Ganz beliebig ist der Methodenaufruf allerdings nicht, denn jede Methode hat eine exakt definierte Parameterliste mit Parametern eines bestimmten Typs. Ein Delegat beschreibt einen Zeiger auf eine Methode, wobei die Typen der Parameterliste der Methode, auf die der Delegat zeigt, mit der Parameterliste der Delegate-Definition übereinstimmen müssen.
In unserem Beispiel werden in der Parameterliste des Delegaten ProcessOperation zwei Parameter vom Typ Double aufgeführt. Damit wäre dieser Delegat in der Lage, jede x-beliebige Methode eines x-beliebigen Objekts aufzurufen – vorausgesetzt, die Methode definiert eine Parameterliste, die genau zwei Double-Argumente erwartet.
|
Die Parameterliste einer Delegate-Definition entspricht der Parameterliste der Methode, auf die der Delegat zeigt.
|
Das ist nicht die einzige Bedingung, die an die Methode gestellt wird, die ein Delegat beschreibt. Der Rückgabewert spielt eine ebenso wichtige Rolle. Im Beispiel des Delegaten ProcessOperation muss die Methode in jedem Fall einen Rückgabewert vom Typ Double haben. Weil sowohl Parameterliste als auch Rückgabetyp durch einen Delegaten eindeutig festgelegt werden, spricht man beim Konstrukt eines Delegaten auch von einem typisierten Funktionszeiger.
Nicht jede Methode hat einen Rückgabewert. Beabsichtigen Sie beispielsweise, einen Delegaten zu definieren, der in der Lage ist, einen Zeiger auf sämtliche Methoden zu beschreiben, die parameterlos sind und keinen Rückgabewert haben, sähe die Definition folgendermaßen aus:
| Public Delegate Sub MyDelegate()
|
Sie können die Definition eines Delegaten mit der Definition einer Klasse vergleichen, denn beide beschreiben einen Typ. Um ein konkretes Objekt zu erhalten, muss zuerst eine Variable vom Typ der Klasse deklariert werden – das ist bei einem Delegaten nicht anders. Im Beispiel dient dazu die Anweisung:
| Dim process As ProcessOperation
|
Damit ist die Variable process vom Typ ProcessOperation deklariert, aber noch nicht initialisiert. Mit anderen Worten: process ist ein Delegat und kann auf eine Methode verweisen, die zwei Double-Argumente erwartet und einen Double als Resultat des Aufrufs zurückliefert. In diesem Moment weiß der Delegat allerdings noch nicht, um welche Methode es sich dabei genau handelt.
Die Initialisierung erfolgt – analog zur Instanziierung einer Klasse – mit dem Operator New unter Angabe des Delegatentyps. Dahinter gibt man in runden Klammern unter Hinzuziehung des AddressOf-Operators den Bezeichner der Methode an, die später vom Delegaten aufgerufen werden soll. In unserem Beispiel handelt es sich um
| process = New ProcessOperation(AddressOf Addition)
|
und
| process = New ProcessOperation(AddressOf Subtraktion);
|
Danach ist dem Delegaten bekannt, welche Methode ausgeführt werden soll: entweder Addition oder Subtraktion. Allerdings wird die Methode, auf die der Delegat in Form eines Zeigers verweist, noch nicht sofort gestartet, denn dazu bedarf es eines Anstoßes durch den Aufruf des Delegaten:
| Dim result As Double = process(input1, input2)
|
Der Aufruf erinnert an den Aufruf einer Methode, dabei wird allerdings der Methodenname (hier: Addition bzw. Subtraktion) durch die Variable vom Typ des Delegaten ersetzt. In den Klammern werden die erforderlichen Argumente an die Methode übergeben.
5.2.2 Zusammenfassung der Arbeitsschritte  
Um eine aufzurufende Methode erst zur Laufzeit festzulegen, ist das Konstrukt der Delegaten sicherlich sehr interessant. Allerdings ist die syntaktische Realisierung etwas gewöhnungsbedürftig, daher sollen hier die Schritte noch einmal allgemein zusammengefasst werden.
| 1. |
Definieren Sie zuerst einen Delegaten, z. B.: |
| |
|
Public Delegate Function ProcessOperation(ByVal dblVar1 _
As Double, ByVal dblVar2 As Double) As Double
Als Modifizierer kommen Private, Protected, Friend und Public in Frage.
| 2. |
Deklarieren Sie eine Variable vom Typ des Delegaten, z. B.: |
| |
|
Dim process As ProcessOperation process
| 3. |
Erzeugen Sie ein Objekt vom Typ des Delegaten, und übergeben Sie dabei als Argument den Namen der Methode, die vom Delegaten aufgerufen werden soll, z. B.: |
| |
|
process = new ProcessOperation(AddressOf Addition)
| 4. |
Rufen Sie den Delegaten auf, und übergeben Sie dabei die Parameter, die von der Methode empfangen werden sollen, auf die der Delegat zeigt, z. B.: |
| |
|
Dim result As double = process(input1, input2)
| Hinweis
|
|
Wir haben noch nicht alle Gesichtspunkte erörtert, die im Zusammenhang mit den Delegaten von programmiertechnischem Interesse sind. Wenn wir uns jedoch weiter mit diesen Konstrukten beschäftigen wollen, sollten mehr Grundsätze der objektorientierten Programmierung bekannt sein. Daher wird das Thema an dieser Stelle unterbrochen und in Kapitel 7 noch einmal aufgegriffen.
|
5.2.3 Vereinfachter Aufruf eines Delegaten  
Neben der gezeigten Notation gibt es unter Visual Basic 2005 eine weitere, einen Delegaten zu instanziieren und ihm gleichzeitig die auszuführende Methode anzugeben. Diese Neuerung ist etwas einfacher in der Handhabung. Sie können nämlich anstelle der Anweisung
| Dim process As ProcessOperation = _
|
| New ProcessOperation(AddressOf Addition)
|
auch wie folgt codieren:
| Dim process As ProcessOperation = AddressOf Addition
|
Auch für den Aufruf des Delegaten gibt es mit
| Dim result As Double = p.Invoke(input1, input2)
|
eine zweite Alternative.
|